﻿using System;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.ComponentModel;
using System.Reflection;
using System.Collections.Generic;
using System.Threading;
using System.Globalization;

namespace Microsoft.Office.OneNote
{
    /// <summary>
    /// Provides functions for reading and writing OneNote XML.
    /// </summary>
    public static class OneNoteSchema
    {
        private const XmlDateTimeSerializationMode DateTimeFormat = XmlDateTimeSerializationMode.RoundtripKind;
        private const String NamespaceUri = "http://schemas.microsoft.com/office/onenote/2007/onenote";

        private static readonly XDeclaration XmlDeclaration = new XDeclaration("1.0", null, null);
        private static readonly Object SyncRoot = new Object();

        private static TwoWayMap<XName, Type> _xmlMap;

        // Object ID

        /// <summary>
        /// Reads the ID attribute of the specified element.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <exception cref=""></exception>
        /// <returns></returns>
        public static OneNoteObjectId GetObjectID(XElement element)
        {
            Type objectType = GetObjectType(element.Name);

            if (objectType == null)
            {
                return OneNoteObjectId.Empty;
            }

            XName idAttributeName = GetObjectIDAttributeName(objectType);

            if (idAttributeName != null)
            {
                var attr = element.Attribute(idAttributeName);

                if (attr != null)
                {
                    if (!string.IsNullOrEmpty(attr.Value))
                    {
                        return OneNoteObjectId.Parse(attr.Value);
                    }
                }
            }

            return OneNoteObjectId.Empty;
        }

        private static XName GetObjectIDAttributeName(Type type)
        {
            if (typeof(OneNoteHierarchyObject).IsAssignableFrom(type))
            {
                return XName.Get("ID");
            }

            if (typeof(OneNotePageObject).IsAssignableFrom(type))
            {
                return XName.Get("objectID");
            }

            OneNoteObjectIdAttribute attr = type.GetAttribute<OneNoteObjectIdAttribute>(true);

            if (attr != null)
            {
                return XName.Get(attr.AttributeName);
            }

            return null;
        }

        // Type mapping

        public static Type GetObjectType(XName elementName)
        {
            EnsureInitialized();

            Type type;

            if (_xmlMap.TryGetValue(elementName, out type))
            {
                return type;
            }

#if THROW_ON_UNKNOWN_ELEMENTS
            throw OneNoteException.UnknownXmlElement(elementName);
#else
#warning    Should throw an error here!

            return (from t in AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())
                         let att = t.GetAttribute<OneNoteXmlElementAttribute>()
                         where att != null && att.LocalName.Equals(elementName.LocalName, StringComparison.Ordinal)
                         select t).FirstOrDefault();
#endif
        }

        public static XName GetElementName(Object obj)
        {
            if (obj != null)
            {
                return GetElementName(obj.GetType());
            }

            return null;
        }

        public static XName GetElementName(Type type)
        {
            EnsureInitialized();

            XName xname;

            if (_xmlMap.TryGetValue(type, out xname))
            {
                return xname;
            }

            return RetrieveElementName(type);
        }

        private static void EnsureInitialized()
        {
            if (_xmlMap == null)
            {
                lock (SyncRoot)
                {
                    Thread.MemoryBarrier();

                    if (_xmlMap == null)
                    {
                        var map = CreateXmlMapping();

                        Thread.MemoryBarrier();
                        _xmlMap = map;
                    }
                }
            }
        }

        private static TwoWayMap<XName, Type> CreateXmlMapping()
        {
            var types = typeof(OneNoteObject).Assembly.GetTypes();
            var map = new TwoWayMap<XName, Type>();

            foreach (Type t in types)
            {
                XName xname = RetrieveElementName(t);

                if (xname != null)
                {
                    map.Add(xname, t);
                }
            }

            return map;
        }

        private static XName RetrieveElementName(Type type)
        {
            var onxea = type.GetAttribute<OneNoteXmlElementAttribute>();

            if (onxea != null)
            {
                return XName.Get(onxea.LocalName, NamespaceUri);
            }

            return null;
        }
        
        // Deserialization

        public static OneNoteObject Deserialize(XElement element)
        {
            Type type = GetObjectType(element.Name);
            OneNoteObject obj = null;

            if (typeof(OneNoteObjectWithIdentity).IsAssignableFrom(type))
            {
                OneNoteObjectId objId = GetObjectID(element);
                obj = OneNoteObjectFactory.CreateWithIdentity(type, objId);
            }
            else
            {
                obj = OneNoteObjectFactory.Create(element.Name);
            }

            Deserialize(obj, element);
            return obj;
        }

        public static void Deserialize(OneNoteObject obj, XElement element)
        {
            // stores the elements which don't represent a property
            LinkedList<XElement> unprocessed = new LinkedList<XElement>(element.Elements());

            obj.SetFlags(true, OneNoteObjectFlags.Deserializing);

            try
            {
                foreach (var property in OneNoteProperty.GetProperties(obj))
                {
                    Object result = null;

                    if (property.CheckFlagsAnd(OneNotePropertyFlags.IsAttribute))
                    {
                        result = DeserializeAttribute(property, element);
                    }
                    else if (property.CheckFlagsAnd(OneNotePropertyFlags.IsElement))
                    {
                        result = DeserializeElement(obj, property, element, unprocessed);
                    }

                    if (result != null)
                    {
                        obj.DeserializeValue(property, result);
                    }
                }

                IOneNoteSchemaCallback callback = (obj as IOneNoteSchemaCallback);

                if (callback != null)
                {
                    callback.Deserialize(element);
                }
            }
            finally
            {
                obj.SetFlags(false, OneNoteObjectFlags.Deserializing);
            }

            DeserializeChildren(obj, unprocessed);
        }

        private static void DeserializeChildren(OneNoteObject obj, LinkedList<XElement> unprocessed)
        {
            IOneNoteObjectWithChildren storage = (obj as IOneNoteObjectWithChildren);

            if (storage != null && unprocessed.Count > 0)
            {
                storage.UpdateFrom(unprocessed);
            }
        }

        private static Object DeserializeElement(OneNoteObject obj, OneNoteProperty property, XElement element, LinkedList<XElement> unprocessed)
        {
            XName name = GetElementName(property.PropertyType);
            XElement innerElement = element.Element(name);

            if (innerElement != null)
            {
                unprocessed.Remove(innerElement);

                OneNoteObject innerObject = (obj.GetValue(property) as OneNoteObject);

                if (innerObject == null)
                {
                    innerObject = OneNoteObjectFactory.Create(property.PropertyType);
                }

                Deserialize(innerObject, innerElement);

                return innerObject;
            }

            return null;
        }

        private static Object DeserializeAttribute(OneNoteProperty property, XElement element)
        {
            XAttribute attribute = element.Attribute(property.Name);

            if (attribute != null)
            {
                return ConvertFromString(property.PropertyType, attribute.Value);
            }

            return null;
        }

        // Serialization

        public static XElement CreateElement(Object obj)
        {
            XName name = GetElementName(obj);

            if (name == null)
            {
                return null;
            }

            OneNoteObjectWithIdentity idobj = (obj as OneNoteObjectWithIdentity);

            if (idobj == null)
            {
                return new XElement(name);
            }

            if (idobj.ObjectId == OneNoteObjectId.Empty)
            {
                throw new ArgumentException();
            }

            XName idAttributeName = GetObjectIDAttributeName(idobj.GetType());
            XAttribute idAttribute = new XAttribute(idAttributeName, idobj.ObjectId);

            return new XElement(name, idAttribute);
        }

        public static XDocument CreateDocument(params object[] content)
        {
            return new XDocument(XmlDeclaration, content);
        }

        public static XElement Serialize(OneNoteObject obj)
        {
            XElement element = CreateElement(obj);

            SerializeAllProperties(obj, element);
            
            IOneNoteSchemaCallback callback = (obj as IOneNoteSchemaCallback);

            if (callback != null)
            {
                callback.Serialize(element);
            }

            return element;
        }

        public static void SerializeAllProperties(OneNoteObject obj, XElement element)
        {
            SerializeProperties(obj, element, OneNoteProperty.GetProperties(obj));
        }

        public static void SerializeDirtyProperties(OneNoteObject obj, XElement element)
        {
            SerializeProperties(obj, element, obj.GetDirtyProperties());
        }

        public static void SerializeProperties(OneNoteObject obj, XElement element, IEnumerable<OneNoteProperty> properties)
        {
            element.Add(properties.Select(p => SerializeProperty(obj, p)));
        }

        private static XObject SerializeProperty(OneNoteObject obj, OneNoteProperty property)
        {
            if (property.CheckFlagsAnd(OneNotePropertyFlags.IsAttribute))
            {
                // serialize an attribute

                Object value = obj.GetValue(property);
                return new XAttribute(property.Name, ConvertToString(value));
            }

            if (property.CheckFlagsAnd(OneNotePropertyFlags.IsElement))
            {
                // serialize an element

                OneNoteObject value = (obj.GetValue(property) as OneNoteObject);

                if (value != null)
                {
                    return Serialize(value);
                }
            }

            return null;
        }

        // String converting

        private static object ConvertFromString(Type targetType, string str)
        {
            if (targetType == typeof(String))
            {
                return str;
            }

            if (targetType == typeof(DateTime))
            {
                return XmlConvert.ToDateTime(str, DateTimeFormat);
            }

            if (targetType == typeof(Boolean))
            {
                if (str.Equals("1"))
                {
                    return true;
                }

                if (str.Equals("0"))
                {
                    return false;
                }
            }

            TypeConverter converter = TypeDescriptor.GetConverter(targetType);

            if (converter.CanConvertFrom(typeof(String)))
            {
                return converter.ConvertFromInvariantString(str);
            }

            return null;
        }

        private static string ConvertToString(object obj)
        {
            if (obj == null)
            {
                return string.Empty;
            }

            String str = (obj as String);
            if (str != null)
            {
                return str;
            }

            CultureInfo culture = (obj as CultureInfo);
            if (culture != null)
            {
                return culture.TwoLetterISOLanguageName;
            }

            if (obj is DateTime)
            {
                return XmlConvert.ToString((DateTime)(obj), DateTimeFormat);
            }

            TypeConverter converter = TypeDescriptor.GetConverter(obj);

            if (converter.CanConvertTo(typeof(String)))
            {
                return converter.ConvertToInvariantString(obj);
            }

            return obj.ToString();
        }
    }
}
